// --------------------------------------------------------------------------------------------------------------------
// <copyright file="AsynchronousTestContext.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon.Testing.Threading
{
    using System;
    using System.Collections.Generic;
    using System.Threading;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    public class AsynchronousTestContext : IDisposable
    {
        #region Fields

        private static AsynchronousTestContext _current;
        private readonly Queue<IAsynchronousTestActivity> _workItems = new Queue<IAsynchronousTestActivity>();
        private IAsynchronousTestActivity _currentActivity;
        private ThreadDispatcher _dispatcher;
        private bool _isDisposed;

        #endregion

        private AsynchronousTestContext()
        {
        }

        public static AsynchronousTestContext Current
        {
            get { return _current; }
        }

        #region IDisposable Members

        void IDisposable.Dispose()
        {
            // already disposing
            if (_isDisposed)
            {
                throw new ObjectDisposedException(typeof(AsynchronousTestContext).FullName);
            }

            _isDisposed = true;

            // never initialized
            if (_dispatcher == null)
            {
                return;
            }

            try
            {
                if (_dispatcher.CheckAccess())
                {
                    // signal the dispatcher to close (but can't wait as on same thread)
                    _dispatcher.Stop(false);

                    // throw deadlock detection
                    throw new InvalidOperationException("Deadlock detected");
                }

                // wait for completion
                _dispatcher.Join();

                // if there was an exception then raise it
                if (_dispatcher.Error != null)
                {
                    Assert.Fail(_dispatcher.Error + "\r\n" + _dispatcher.Error.StackTrace);
                }
            }
            finally
            {
                // unhook dispatcher
                _dispatcher = null;

                // unhook context (dispatcher is only non-null if context is this)
                Interlocked.Exchange(ref _current, null);
            }
        }

        #endregion

        private void Initialize()
        {
            _dispatcher = new ThreadDispatcher(false);
            _dispatcher.UnhandledException += OnUnhandledException;
            _dispatcher.Idle += DispatcherIdle;
            _dispatcher.Start();
        }

        private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            if (UnhandledException == null)
            {
                throw (Exception)e.ExceptionObject;
            }

            UnhandledException(this, e);
        }

        private void DispatcherIdle(object sender, EventArgs e)
        {
            // activities are polled
            if (_currentActivity != null && !_currentActivity.IsCompleted)
            {
                _dispatcher.SendOrPost(() => _currentActivity.Execute());
            }
            else
            {
                if (IsCompleted())
                {
                    _dispatcher.Stop(false);
                }
                else
                {
                    DequeueActivity();
                }
            }
        }

        private bool IsCompleted()
        {
            return _currentActivity == CompletedTestActivity.Instance;
        }

        private void DequeueActivity()
        {
            _currentActivity = null;
            lock (_workItems)
            {
                // dequeue next item, it will be picked up on the next idle run
                if (_workItems.Count > 0)
                {
                    _currentActivity = _workItems.Dequeue();
                    _currentActivity.Reset();
                }
            }
        }

        public void Enqueue(IAsynchronousTestActivity activity)
        {
            Guard.ArgumentNull("activity", activity);
            lock (_workItems)
            {
                _workItems.Enqueue(activity);
            }
        }

        public static IDisposable Create()
        {
            var result = new AsynchronousTestContext();
            if (Interlocked.CompareExchange(ref _current, result, null) != null)
            {
                throw new InvalidOperationException("Only one SilverlightTestContext can active at any one time.");
            }

            result.Initialize();
            return result;
        }

        public event UnhandledExceptionEventHandler UnhandledException;
    }
}